DynamoDBの永続アトリビュートのあるAlexaスキルを作ってみた(Python)
こんにちは、3/22にAlexaスキルビルダーが廃止になるという事で、アレクサを勉強することにしたリサリサです。
アレクサ道場を一通り見たので、とりあえず、簡単なスキルを作ってみました。アレクサのコードエディタを使ったのですが、DynamoDBにテーブルを作ったりする必要すらなく、本当にとても簡単に作れて感動したのでブログにしてみます!
作ったもの
毎月遊びに使うのは3万円と決めてはいるものの、家計のお財布と遊びのお財布を分けておらず、結局いくら使っているか分からない状態になっていたので、今月いくらお金を使えるのかを教えてくれるスキルを作ってみました。
DynamoDBに月のお小遣いと今月使える残高を持ち、それを更新していくだけのシンプルなスキルです。機能は以下の三つだけ。
起動時:今月の残りお小遣いを教えてくれる
設定:月に使えるお小遣いの金額を設定できる。残高リセット
お小遣い使用:お小遣いの使用を聞いて、残高を減らす
作ってみた
テンプレートを作成
モデルは「カスタムモデル」。バックエンドは「Alexa-hosted(Python)」にしました。
「スクラッチで作成」を選択し「テンプレートで続ける」。
1分ほど待つとテンプレートが完成します。以下のように、コードも既にたくさんかかれており、これを少しいじるだけで簡単なスキルは作れてしまいます。
そのままハローワールドを試すとこんな感じになります。
最初の○○を開いての○○はデフォルトはスキル名になっています。
インテントを書き換え
各機能ごとにをインテントというものを作ります。何を喋ったらその機能が動くのか、何をインプットで受け付けるのかを定義するものです。
今回の場合は、以下の2機能に対して作りました。
設定(SettingIntent):月に使えるお小遣いの金額を設定できる
お小遣い使用(PayIntent):お小遣いの使用を登録できる。残高を減らす
SettingIntent
「月のお小遣いは○○円」いう発話を受けると設定機能が動作します。○○円は数値で、この数値をpocketmoneyという変数(スロットといいます)で受け取るという設定をします。
PayIntent
「○○円使った」という発話を受けると設定機能が動作します。これもpayというスロットで受け取ります。
コードを書き換え
DynamoDBのアダプター追加
DynamoDBを使いたいので、以下の手順に沿って、それぞれコードを追加します。最後にコード全文を載せるので、手順の紹介は割愛します。
https://developer.amazon.com/ja-JP/docs/alexa/hosted-skills/alexa-hosted-skills-session-persistence.html
インテントに合わせてClassを追加
class SettingIntentHandler(AbstractRequestHandler): """Handler for Setting Intent.""" def can_handle(self, handler_input): # type: (HandlerInput) -> bool return ask_utils.is_intent_name("SettingIntent")(handler_input) def handle(self, handler_input): # type: (HandlerInput) -> Response pocketmoney = ask_utils.get_slot_value(handler_input=handler_input, slot_name='pocketmoney') attr = handler_input.attributes_manager.persistent_attributes attr['pocketmoney'] = pocketmoney attr['balance'] = pocketmoney handler_input.attributes_manager.session_attributes = attr handler_input.attributes_manager.save_persistent_attributes() speak_output = f"月のお小遣いを{attr['pocketmoney']}円に設定しました。今月はあと{attr['balance']}円のお小遣いを使えます。" return ( handler_input.response_builder .speak(speak_output) # .ask("add a reprompt if you want to keep the session open for the user to respond") .response ) class PayIntentHandler(AbstractRequestHandler): """Handler for Pay Intent.""" def can_handle(self, handler_input): # type: (HandlerInput) -> bool return ask_utils.is_intent_name("PayIntent")(handler_input) def handle(self, handler_input): # type: (HandlerInput) -> Response pay = ask_utils.get_slot_value(handler_input=handler_input, slot_name="pay") attr = handler_input.attributes_manager.persistent_attributes attr['balance'] = str(int(attr['balance']) - int(pay)) handler_input.attributes_manager.session_attributes = attr handler_input.attributes_manager.save_persistent_attributes() speak_output = f"{pay}円使ったので、今月使える残りのお小遣いは{attr['balance']}円です。" return ( handler_input.response_builder .speak(speak_output) # .ask("add a reprompt if you want to keep the session open for the user to respond") .response )
追加したClassをSkillBuilderに追加
sb = CustomSkillBuilder(persistence_adapter = dynamodb_adapter) sb.add_request_handler(LaunchRequestHandler()) sb.add_request_handler(SettingIntentHandler()) sb.add_request_handler(PayIntentHandler()) sb.add_request_handler(HelpIntentHandler()) sb.add_request_handler(CancelOrStopIntentHandler()) sb.add_request_handler(SessionEndedRequestHandler()) sb.add_request_handler(IntentReflectorHandler()) # make sure IntentReflectorHandler is last so it doesn't override your custom intent handlers sb.add_exception_handler(CatchAllExceptionHandler())
起動時の処理を書き換え
class LaunchRequestHandler(AbstractRequestHandler): """Handler for Skill Launch.""" def can_handle(self, handler_input): # type: (HandlerInput) -> bool return ask_utils.is_request_type("LaunchRequest")(handler_input) def handle(self, handler_input): # type: (HandlerInput) -> Response attr = handler_input.attributes_manager.persistent_attributes if not attr: attr['pocketmoney'] = '30000' attr['balance'] = '30000' handler_input.attributes_manager.session_attributes = attr handler_input.attributes_manager.save_persistent_attributes() speak_output = f"あなたの今月の残りお小遣いは{attr['balance']}円です。" return ( handler_input.response_builder .speak(speak_output) .ask(speak_output) .response )
動かしてみた
100円使ってみる。
お小遣いの設定変えてみる
終了して開きなおしてみる。
できたーー!
せっかくなので、審査にも出してみましたw
追記
すごい簡易的な履歴機能(過去にさかのぼったりできない…)つけて、公開できました!
全文
Pythonが公開されて1年半。サンプルコードがあまり落ちていなくて困ったので、ほぼテンプレートのままですが全文を貼っておきます。どなたかのお役に立てば幸いです。
reauirements.txt
boto3==1.9.216 ask-sdk-core==1.11.0 ask-sdk-dynamodb-persistence-adapter==1.15.0
lambda_function.py
# -*- coding: utf-8 -*- # This sample demonstrates handling intents from an Alexa skill using the Alexa Skills Kit SDK for Python. # Please visit https://alexa.design/cookbook for additional examples on implementing slots, dialog management, # session persistence, api calls, and more. # This sample is built using the handler classes approach in skill builder. import logging import ask_sdk_core.utils as ask_utils from ask_sdk_core.skill_builder import SkillBuilder,CustomSkillBuilder from ask_sdk_core.dispatch_components import AbstractRequestHandler from ask_sdk_core.dispatch_components import AbstractExceptionHandler from ask_sdk_core.handler_input import HandlerInput from ask_sdk_model import Response import os import boto3 from ask_sdk_dynamodb.adapter import DynamoDbAdapter logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) class LaunchRequestHandler(AbstractRequestHandler): """Handler for Skill Launch.""" def can_handle(self, handler_input): # type: (HandlerInput) -> bool return ask_utils.is_request_type("LaunchRequest")(handler_input) def handle(self, handler_input): # type: (HandlerInput) -> Response attr = handler_input.attributes_manager.persistent_attributes if not attr: attr['pocketmoney'] = '30000' attr['balance'] = '30000' handler_input.attributes_manager.session_attributes = attr handler_input.attributes_manager.save_persistent_attributes() speak_output = f"あなたの今月の残りお小遣いは{attr['balance']}円です。" return ( handler_input.response_builder .speak(speak_output) .ask(speak_output) .response ) class SettingIntentHandler(AbstractRequestHandler): """Handler for Setting Intent.""" def can_handle(self, handler_input): # type: (HandlerInput) -> bool return ask_utils.is_intent_name("SettingIntent")(handler_input) def handle(self, handler_input): # type: (HandlerInput) -> Response pocketmoney = ask_utils.get_slot_value(handler_input=handler_input, slot_name='pocketmoney') attr = handler_input.attributes_manager.persistent_attributes attr['pocketmoney'] = pocketmoney attr['balance'] = pocketmoney handler_input.attributes_manager.session_attributes = attr handler_input.attributes_manager.save_persistent_attributes() speak_output = f"月のお小遣いを{attr['pocketmoney']}円に設定しました。今月はあと{attr['balance']}円のお小遣いを使えます。" return ( handler_input.response_builder .speak(speak_output) # .ask("add a reprompt if you want to keep the session open for the user to respond") .response ) class PayIntentHandler(AbstractRequestHandler): """Handler for Pay Intent.""" def can_handle(self, handler_input): # type: (HandlerInput) -> bool return ask_utils.is_intent_name("PayIntent")(handler_input) def handle(self, handler_input): # type: (HandlerInput) -> Response pay = ask_utils.get_slot_value(handler_input=handler_input, slot_name="pay") attr = handler_input.attributes_manager.persistent_attributes attr['balance'] = str(int(attr['balance']) - int(pay)) handler_input.attributes_manager.session_attributes = attr handler_input.attributes_manager.save_persistent_attributes() speak_output = f"{pay}円使ったので、今月使える残りのお小遣いは{attr['balance']}円です。" return ( handler_input.response_builder .speak(speak_output) # .ask("add a reprompt if you want to keep the session open for the user to respond") .response ) class HelpIntentHandler(AbstractRequestHandler): """Handler for Help Intent.""" def can_handle(self, handler_input): # type: (HandlerInput) -> bool return ask_utils.is_intent_name("AMAZON.HelpIntent")(handler_input) def handle(self, handler_input): # type: (HandlerInput) -> Response speak_output = "あなたの今月の残りのお小遣いを教えます。月のお小遣いはいくら。と言って頂ければそれを毎月の使用可能額に設定します。いくら使ったと言って頂ければそれを差し引きます。" return ( handler_input.response_builder .speak(speak_output) .ask(speak_output) .response ) class CancelOrStopIntentHandler(AbstractRequestHandler): """Single handler for Cancel and Stop Intent.""" def can_handle(self, handler_input): # type: (HandlerInput) -> bool return (ask_utils.is_intent_name("AMAZON.CancelIntent")(handler_input) or ask_utils.is_intent_name("AMAZON.StopIntent")(handler_input)) def handle(self, handler_input): # type: (HandlerInput) -> Response speak_output = "Goodbye!" return ( handler_input.response_builder .speak(speak_output) .response ) class SessionEndedRequestHandler(AbstractRequestHandler): """Handler for Session End.""" def can_handle(self, handler_input): # type: (HandlerInput) -> bool return ask_utils.is_request_type("SessionEndedRequest")(handler_input) def handle(self, handler_input): # type: (HandlerInput) -> Response # Any cleanup logic goes here. return handler_input.response_builder.response class IntentReflectorHandler(AbstractRequestHandler): """The intent reflector is used for interaction model testing and debugging. It will simply repeat the intent the user said. You can create custom handlers for your intents by defining them above, then also adding them to the request handler chain below. """ def can_handle(self, handler_input): # type: (HandlerInput) -> bool return ask_utils.is_request_type("IntentRequest")(handler_input) def handle(self, handler_input): # type: (HandlerInput) -> Response intent_name = ask_utils.get_intent_name(handler_input) speak_output = "You just triggered " + intent_name + "." return ( handler_input.response_builder .speak(speak_output) # .ask("add a reprompt if you want to keep the session open for the user to respond") .response ) class CatchAllExceptionHandler(AbstractExceptionHandler): """Generic error handling to capture any syntax or routing errors. If you receive an error stating the request handler chain is not found, you have not implemented a handler for the intent being invoked or included it in the skill builder below. """ def can_handle(self, handler_input, exception): # type: (HandlerInput, Exception) -> bool return True def handle(self, handler_input, exception): # type: (HandlerInput, Exception) -> Response logger.error(exception, exc_info=True) speak_output = f"もう一度お願いします。" return ( handler_input.response_builder .speak(speak_output) .ask(speak_output) .response ) # The SkillBuilder object acts as the entry point for your skill, routing all request and response # payloads to the handlers above. Make sure any new handlers or interceptors you've # defined are included below. The order matters - they're processed top to bottom. ddb_region = os.environ.get('DYNAMODB_PERSISTENCE_REGION') ddb_table_name = os.environ.get('DYNAMODB_PERSISTENCE_TABLE_NAME') ddb_resource = boto3.resource('dynamodb', region_name=ddb_region) dynamodb_adapter = DynamoDbAdapter(table_name=ddb_table_name, create_table=False, dynamodb_resource=ddb_resource) sb = CustomSkillBuilder(persistence_adapter = dynamodb_adapter) sb.add_request_handler(LaunchRequestHandler()) sb.add_request_handler(SettingIntentHandler()) sb.add_request_handler(PayIntentHandler()) sb.add_request_handler(HelpIntentHandler()) sb.add_request_handler(CancelOrStopIntentHandler()) sb.add_request_handler(SessionEndedRequestHandler()) sb.add_request_handler(IntentReflectorHandler()) # make sure IntentReflectorHandler is last so it doesn't override your custom intent handlers sb.add_exception_handler(CatchAllExceptionHandler()) lambda_handler = sb.lambda_handler()